﻿/* Copyright (C) 2007-2010 STS Soft

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; version 2 of the License.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA */

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using STSdb.General.Buffers;
using STSdb.General.Collections;
using STSdb.Volumes;

namespace STSdb.Files
{
    /// <summary>
    /// Class is ThreadSafe
    /// </summary>
    public partial class FileSystem : IDisposable
    {
        private bool disposed = false;

        internal static readonly ulong MAX_HANDLE = ulong.MaxValue / FileInfo.SIZE;
        private static readonly ulong CATALOG_HANDLE = MAX_HANDLE + 1;
        private static readonly ulong GARBAGE_HANDLE = MAX_HANDLE + 2;

        private ulong fileCount;
        private ulong sectorCount;

        //system data
        private FileInfo CatalogInfo;
        private FileInfo GarbageInfo;

        //system files
        private RawFile CatalogFile;
        private RawFile GarbageFile;
        private BinaryFile BinaryGarbageFile;

        //Handle -> RawFile
        private Dictionary<ulong, RawFile> files = new Dictionary<ulong, RawFile>();
        //SectorID -> Node
        internal Cache<ulong, Node> Cache = new Cache<ulong, Node>(1024);
        //synchronizes access to CatalogFile, GarbageFile и RecycleFile
        internal readonly object SyncRoot = new object();
        //this instance must be ThreadSafe (here is the place to make logical RAID)
        public readonly IVolume Volume;

        public FileSystem(IVolume volume)
        {
            if (volume == null)
                throw new ArgumentNullException("volume");

            Cache.Overflow += new Cache<ulong, Node>.OverflowDelegate(Cache_Overflow);

            Volume = volume;
            sectorCount = (ulong)Math.Ceiling(Volume.Length / (double)Sector.SIZE_64);
            if (sectorCount == 0)
                sectorCount = 1;

            if (Volume.Length > 0)
            {
                ReadSystemData();
                CatalogFile = new RawFile(this, CATALOG_HANDLE, CatalogInfo.SectorID, CatalogInfo.Length);
                GarbageFile = new RawFile(this, GARBAGE_HANDLE, GarbageInfo.SectorID, GarbageInfo.Length);
            }
            else
            {
                CatalogFile = new RawFile(this, CATALOG_HANDLE);
                GarbageFile = new RawFile(this, GARBAGE_HANDLE);
            }
            BinaryGarbageFile = new BinaryFile(GarbageFile);

            RecycleFile = FindRecycleFile(0);
        }

        #region IDisposable Members

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    lock (SyncRoot)
                    {
                        if (RecycleFile != null)
                            RecycleFile.Close();

                        foreach (RawFile file in files.Values.ToArray())
                            file.Close();

                        GarbageFile.Close();
                        CatalogFile.Close();

                        WriteSystemData();
                    }

                    Volume.Close();
                }
            }
            disposed = true;
        }

        ~FileSystem()
        {
            Dispose(false);
        }

        protected void CheckDisposal()
        {
            if (this.disposed)
                throw new ObjectDisposedException("Repository is disposed.");
        }

        #endregion

        #region Manage

        public RawFile CreateFile()
        {
            lock (SyncRoot)
            {
                //searches for first deleted file
                ulong handle;
                if (!BinaryGarbageFile.FindFirstBit(0, HandleCount, true, out handle))
                    handle = HandleCount; //new handle
                else
                {
                    FileInfo fi = GetFileInfo(handle);
                    if (fi.SectorID != Node.INVALID_SECTOR_ID) //the file still recycles
                        handle = HandleCount; //new handle
                }

                //the new file is registered as a non-deleted
                BinaryGarbageFile.SetBit(handle, false);

                RawFile file = new RawFile(this, handle);
                file.References++;
                SetFileInfo(file.Handle, new FileInfo(file.RootSectorID, file.Length));

                FileCount++;
                files.Add(file.Handle, file);
                return file;
            }
        }

        public RawFile OpenFile(ulong handle)
        {
            lock (SyncRoot)
            {
                if (handle >= HandleCount)
                    throw new FileNotFoundException(handle.ToString());

                RawFile file;
                if (files.TryGetValue(handle, out file))
                {
                    file.References++;
                    return file;
                }

                if (BinaryGarbageFile.GetBit(handle)) //the file is deleted
                    throw new FileNotFoundException(handle.ToString());

                FileInfo fi = GetFileInfo(handle);
                file = new RawFile(this, handle, fi.SectorID, fi.Length);
                file.References++;

                files.Add(file.Handle, file);
                return file;
            }
        }

        public bool ExistsFile(ulong handle)
        {
            lock (SyncRoot)
                return handle < HandleCount && !BinaryGarbageFile.GetBit(handle) && GetFileInfo(handle).SectorID != Node.INVALID_SECTOR_ID;
        }

        public RawFile ObtainFile(ulong handle)
        {
            lock (SyncRoot)
            {
                if (ExistsFile(handle))
                    return OpenFile(handle);
                else
                    return CreateFile();
            }
        }

        public void DeleteFile(ulong handle)
        {
            lock (SyncRoot)
            {
                if (handle >= HandleCount)
                    throw new FileNotFoundException(handle.ToString());

                if (files.ContainsKey(handle))
                    throw new AccessViolationException("File is opened.");

                if (!BinaryGarbageFile.GetBit(handle))
                {
                    BinaryGarbageFile.SetBit(handle, true);
                    FileCount--;

                    if (RecycleFile == null)
                    {
                        FileInfo fi = GetFileInfo(handle);
                        if (fi.SectorID != Node.INVALID_SECTOR_ID)
                            RecycleFile = new RawFile(this, handle, fi.SectorID, fi.Length);
                    }
                }
            }
        }

        internal void CloseFile(RawFile file)
        {
            lock (SyncRoot)
            {
                file.References--;
                file.References = 0;
                if (file.References == 0)
                    files.Remove(file.Handle);
            }
        }

        public void Repair()
        {
            lock (SyncRoot)
            {
                FileCount = 0;
                for (ulong handle = 0; handle < HandleCount; handle++)
                {
                    FileInfo fi = GetFileInfo(handle);
                    if (fi.SectorID == Node.INVALID_SECTOR_ID && !BinaryGarbageFile.GetBit(handle))
                        BinaryGarbageFile.SetBit(handle, true);
                    else
                        FileCount++;
                }
            }
        }

        public ulong FileCount
        {
            get
            {
                lock (SyncRoot)
                    return fileCount;
            }
            private set
            {
                lock (SyncRoot)
                    fileCount = value;
            }
        }

        private ulong HandleCount
        {
            get
            {
                lock (SyncRoot)
                    return CatalogFile.Length / FileInfo.SIZE;
            }
        }

        /// <summary>
        /// Flush system data - catalog, garbage and system files
        /// </summary>
        public void Flush()
        {
            lock (SyncRoot)
            {
                if (RecycleFile != null)
                    RecycleFile.Flush();

                GarbageFile.Flush();
                CatalogFile.Flush();

                WriteSystemData();
            }

            Volume.Flush();
        }

        private void WriteSystemData()
        {
            const ulong SYSTEM_SECTOR_ID = 0;
            byte[] buffer;

            using (MemoryStream stream = new MemoryStream((int)(FileInfo.SIZE + FileInfo.SIZE + sizeof(ulong))))
            {
                BinaryWriter writer = new BinaryWriter(stream);
                CatalogInfo = new FileInfo(CatalogFile.RootSectorID, CatalogFile.Length);
                GarbageInfo = new FileInfo(GarbageFile.RootSectorID, GarbageFile.Length);

                buffer = StructurePrimitives.StructToByteArray(CatalogInfo);
                writer.Write(buffer);
                buffer = StructurePrimitives.StructToByteArray(GarbageInfo);
                writer.Write(buffer);
                writer.Write(FileCount);

                Volume.Write(SYSTEM_SECTOR_ID * Sector.SIZE_64, stream.GetBuffer(), 0, (int)stream.Length);
            }
        }

        private void ReadSystemData()
        {
            const ulong SYSTEM_SECTOR_ID = 0;
            byte[] buffer = new byte[FileInfo.SIZE + FileInfo.SIZE + sizeof(ulong)];

            Volume.Read(SYSTEM_SECTOR_ID * Sector.SIZE_64, buffer, 0, buffer.Length);

            using (MemoryStream stream = new MemoryStream(buffer))
            {
                BinaryReader reader = new BinaryReader(stream);
                buffer = reader.ReadBytes((int)FileInfo.SIZE);
                CatalogInfo = (FileInfo)StructurePrimitives.ByteArrayToStruct(buffer, typeof(FileInfo));
                buffer = reader.ReadBytes((int)FileInfo.SIZE);
                GarbageInfo = (FileInfo)StructurePrimitives.ByteArrayToStruct(buffer, typeof(FileInfo));
                FileCount = reader.ReadUInt64();
            }
        }

        internal FileInfo GetFileInfo(ulong handle)
        {
            byte[] buffer = new byte[FileInfo.SIZE];
            CatalogFile.Read(handle * FileInfo.SIZE, buffer, 0, buffer.Length);

            return (FileInfo)StructurePrimitives.ByteArrayToStruct(buffer, typeof(FileInfo));
        }

        internal void SetFileInfo(ulong handle, FileInfo fileInfo)
        {
            byte[] buffer = StructurePrimitives.StructToByteArray(fileInfo);
            CatalogFile.Write(handle * FileInfo.SIZE, buffer, 0, buffer.Length);
        }

        #endregion

        #region Cache

        private void Cache_Overflow(KeyValuePair<ulong, Node> item)
        {
            //anyway here is locked
            if (item.Value.IsModified)
                item.Value.Save();
        }

        public uint CacheCapacity
        {
            get
            {
                lock (Cache)
                    return Cache.Capacity;
            }
            set
            {
                lock (Cache)
                    Cache.Capacity = value;
            }
        }

        #endregion

        public static FileSystem FromMemory(ulong quota)
        {
            return new FileSystem(new MemoryVolume(quota));
        }

        /// <summary>
        /// Creates new or opened existing file system
        /// </summary>
        /// <param name="fileName">Path to the file. If fileName directory part is null or empty method looking in current directory. </param>
        /// <returns>New instance of BaseRepository</returns>
        public static FileSystem FromFile(string fileName)
        {
            if (string.IsNullOrEmpty(fileName))
                throw new ArgumentNullException("fileName");

            string path = Path.GetDirectoryName(fileName);
            if (!Directory.Exists(path) && !String.IsNullOrEmpty(path))
                Directory.CreateDirectory(path);

            VolumeChain volumeChain = new VolumeChain();
            volumeChain.Add(System.IO.File.Exists(fileName) ? new Volume(fileName) : new Volume(fileName, 0, ulong.MaxValue / 2));

            return new FileSystem(volumeChain);
        }

        /// <summary>
        /// Creates chain of files (volumes)
        /// </summary>
        /// <param name="quotaExceedHandler">on new volume needed</param>
        /// <returns></returns>
        public static FileSystem FromChain(VolumeChain volumeChain)
        {
            return new FileSystem(volumeChain);
        }

        internal struct FileInfo
        {
            public static readonly ulong SIZE = (ulong)Marshal.SizeOf(typeof(FileInfo));

            public ulong SectorID;
            public ulong Length;

            public FileInfo(ulong sectorID, ulong length)
            {
                SectorID = sectorID;
                Length = length;
            }
        }
    }
}
